import { Lexer } from "./Lexer.js";
import { Parser } from "./Parser.js";
import { Tokenizer } from "./Tokenizer.js";
import { Renderer } from "./Renderer.js";
import { TextRenderer } from "./TextRenderer.js";
import { Slugger } from "./Slugger.js";
import { Hooks } from "./Hooks.js";
import { checkSanitizeDeprecation, escape } from "./helpers.js";
import { getDefaults, changeDefaults, defaults } from "./defaults.js";

function onError(silent, async, callback) {
	return e => {
		e.message += "\nPlease report this to https://github.com/markedjs/marked.";

		if (silent) {
			const msg =
				"<p>An error occurred:</p><pre>" +
				escape(e.message + "", true) +
				"</pre>";
			if (async) {
				return Promise.resolve(msg);
			}
			if (callback) {
				callback(null, msg);
				return;
			}
			return msg;
		}

		if (async) {
			return Promise.reject(e);
		}
		if (callback) {
			callback(e);
			return;
		}
		throw e;
	};
}

function parseMarkdown(lexer, parser) {
	return (src, opt, callback) => {
		if (typeof opt === "function") {
			callback = opt;
			opt = null;
		}

		const origOpt = { ...opt };
		opt = { ...marked.defaults, ...origOpt };
		const throwError = onError(opt.silent, opt.async, callback);

		// throw error in case of non string input
		if (typeof src === "undefined" || src === null) {
			return throwError(
				new Error("marked(): input parameter is undefined or null")
			);
		}
		if (typeof src !== "string") {
			return throwError(
				new Error(
					"marked(): input parameter is of type " +
						Object.prototype.toString.call(src) +
						", string expected"
				)
			);
		}

		checkSanitizeDeprecation(opt);

		if (opt.hooks) {
			opt.hooks.options = opt;
		}

		if (callback) {
			const highlight = opt.highlight;
			let tokens;

			try {
				if (opt.hooks) {
					src = opt.hooks.preprocess(src);
				}
				tokens = lexer(src, opt);
			} catch (e) {
				return throwError(e);
			}

			const done = function (err) {
				let out;

				if (!err) {
					try {
						if (opt.walkTokens) {
							marked.walkTokens(tokens, opt.walkTokens);
						}
						out = parser(tokens, opt);
						if (opt.hooks) {
							out = opt.hooks.postprocess(out);
						}
					} catch (e) {
						err = e;
					}
				}

				opt.highlight = highlight;

				return err ? throwError(err) : callback(null, out);
			};

			if (!highlight || highlight.length < 3) {
				return done();
			}

			delete opt.highlight;

			if (!tokens.length) return done();

			let pending = 0;
			marked.walkTokens(tokens, function (token) {
				if (token.type === "code") {
					pending++;
					setTimeout(() => {
						highlight(token.text, token.lang, function (err, code) {
							if (err) {
								return done(err);
							}
							if (code != null && code !== token.text) {
								token.text = code;
								token.escaped = true;
							}

							pending--;
							if (pending === 0) {
								done();
							}
						});
					}, 0);
				}
			});

			if (pending === 0) {
				done();
			}

			return;
		}

		if (opt.async) {
			return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src)
				.then(src => lexer(src, opt))
				.then(tokens =>
					opt.walkTokens
						? Promise.all(marked.walkTokens(tokens, opt.walkTokens)).then(
								() => tokens
						  )
						: tokens
				)
				.then(tokens => parser(tokens, opt))
				.then(html => (opt.hooks ? opt.hooks.postprocess(html) : html))
				.catch(throwError);
		}

		try {
			if (opt.hooks) {
				src = opt.hooks.preprocess(src);
			}
			const tokens = lexer(src, opt);
			if (opt.walkTokens) {
				marked.walkTokens(tokens, opt.walkTokens);
			}
			let html = parser(tokens, opt);
			if (opt.hooks) {
				html = opt.hooks.postprocess(html);
			}
			return html;
		} catch (e) {
			return throwError(e);
		}
	};
}

/**
 * Marked
 */
export function marked(src, opt, callback) {
	return parseMarkdown(Lexer.lex, Parser.parse)(src, opt, callback);
}

/**
 * Options
 */

marked.options = marked.setOptions = function (opt) {
	marked.defaults = { ...marked.defaults, ...opt };
	changeDefaults(marked.defaults);
	return marked;
};

marked.getDefaults = getDefaults;

marked.defaults = defaults;

/**
 * Use Extension
 */

marked.use = function (...args) {
	const extensions = marked.defaults.extensions || {
		renderers: {},
		childTokens: {}
	};

	args.forEach(pack => {
		// copy options to new object
		const opts = { ...pack };

		// set async to true if it was set to true before
		opts.async = marked.defaults.async || opts.async || false;

		// ==-- Parse "addon" extensions --== //
		if (pack.extensions) {
			pack.extensions.forEach(ext => {
				if (!ext.name) {
					throw new Error("extension name required");
				}
				if (ext.renderer) {
					// Renderer extensions
					const prevRenderer = extensions.renderers[ext.name];
					if (prevRenderer) {
						// Replace extension with func to run new extension but fall back if false
						extensions.renderers[ext.name] = function (...args) {
							let ret = ext.renderer.apply(this, args);
							if (ret === false) {
								ret = prevRenderer.apply(this, args);
							}
							return ret;
						};
					} else {
						extensions.renderers[ext.name] = ext.renderer;
					}
				}
				if (ext.tokenizer) {
					// Tokenizer Extensions
					if (!ext.level || (ext.level !== "block" && ext.level !== "inline")) {
						throw new Error("extension level must be 'block' or 'inline'");
					}
					if (extensions[ext.level]) {
						extensions[ext.level].unshift(ext.tokenizer);
					} else {
						extensions[ext.level] = [ext.tokenizer];
					}
					if (ext.start) {
						// Function to check for start of token
						if (ext.level === "block") {
							if (extensions.startBlock) {
								extensions.startBlock.push(ext.start);
							} else {
								extensions.startBlock = [ext.start];
							}
						} else if (ext.level === "inline") {
							if (extensions.startInline) {
								extensions.startInline.push(ext.start);
							} else {
								extensions.startInline = [ext.start];
							}
						}
					}
				}
				if (ext.childTokens) {
					// Child tokens to be visited by walkTokens
					extensions.childTokens[ext.name] = ext.childTokens;
				}
			});
			opts.extensions = extensions;
		}

		// ==-- Parse "overwrite" extensions --== //
		if (pack.renderer) {
			const renderer = marked.defaults.renderer || new Renderer();
			for (const prop in pack.renderer) {
				const prevRenderer = renderer[prop];
				// Replace renderer with func to run extension, but fall back if false
				renderer[prop] = (...args) => {
					let ret = pack.renderer[prop].apply(renderer, args);
					if (ret === false) {
						ret = prevRenderer.apply(renderer, args);
					}
					return ret;
				};
			}
			opts.renderer = renderer;
		}
		if (pack.tokenizer) {
			const tokenizer = marked.defaults.tokenizer || new Tokenizer();
			for (const prop in pack.tokenizer) {
				const prevTokenizer = tokenizer[prop];
				// Replace tokenizer with func to run extension, but fall back if false
				tokenizer[prop] = (...args) => {
					let ret = pack.tokenizer[prop].apply(tokenizer, args);
					if (ret === false) {
						ret = prevTokenizer.apply(tokenizer, args);
					}
					return ret;
				};
			}
			opts.tokenizer = tokenizer;
		}

		// ==-- Parse Hooks extensions --== //
		if (pack.hooks) {
			const hooks = marked.defaults.hooks || new Hooks();
			for (const prop in pack.hooks) {
				const prevHook = hooks[prop];
				if (Hooks.passThroughHooks.has(prop)) {
					hooks[prop] = arg => {
						if (marked.defaults.async) {
							return Promise.resolve(pack.hooks[prop].call(hooks, arg)).then(
								ret => {
									return prevHook.call(hooks, ret);
								}
							);
						}

						const ret = pack.hooks[prop].call(hooks, arg);
						return prevHook.call(hooks, ret);
					};
				} else {
					hooks[prop] = (...args) => {
						let ret = pack.hooks[prop].apply(hooks, args);
						if (ret === false) {
							ret = prevHook.apply(hooks, args);
						}
						return ret;
					};
				}
			}
			opts.hooks = hooks;
		}

		// ==-- Parse WalkTokens extensions --== //
		if (pack.walkTokens) {
			const walkTokens = marked.defaults.walkTokens;
			opts.walkTokens = function (token) {
				let values = [];
				values.push(pack.walkTokens.call(this, token));
				if (walkTokens) {
					values = values.concat(walkTokens.call(this, token));
				}
				return values;
			};
		}

		marked.setOptions(opts);
	});
};

/**
 * Run callback for every token
 */

marked.walkTokens = function (tokens, callback) {
	let values = [];
	for (const token of tokens) {
		values = values.concat(callback.call(marked, token));
		switch (token.type) {
			case "table": {
				for (const cell of token.header) {
					values = values.concat(marked.walkTokens(cell.tokens, callback));
				}
				for (const row of token.rows) {
					for (const cell of row) {
						values = values.concat(marked.walkTokens(cell.tokens, callback));
					}
				}
				break;
			}
			case "list": {
				values = values.concat(marked.walkTokens(token.items, callback));
				break;
			}
			default: {
				if (
					marked.defaults.extensions &&
					marked.defaults.extensions.childTokens &&
					marked.defaults.extensions.childTokens[token.type]
				) {
					// Walk any extensions
					marked.defaults.extensions.childTokens[token.type].forEach(function (
						childTokens
					) {
						values = values.concat(
							marked.walkTokens(token[childTokens], callback)
						);
					});
				} else if (token.tokens) {
					values = values.concat(marked.walkTokens(token.tokens, callback));
				}
			}
		}
	}
	return values;
};

/**
 * Parse Inline
 * @param {string} src
 */
marked.parseInline = parseMarkdown(Lexer.lexInline, Parser.parseInline);

/**
 * Expose
 */
marked.Parser = Parser;
marked.parser = Parser.parse;
marked.Renderer = Renderer;
marked.TextRenderer = TextRenderer;
marked.Lexer = Lexer;
marked.lexer = Lexer.lex;
marked.Tokenizer = Tokenizer;
marked.Slugger = Slugger;
marked.Hooks = Hooks;
marked.parse = marked;

export const options = marked.options;
export const setOptions = marked.setOptions;
export const use = marked.use;
export const walkTokens = marked.walkTokens;
export const parseInline = marked.parseInline;
export const parse = marked;
export const parser = Parser.parse;
export const lexer = Lexer.lex;
export { defaults, getDefaults } from "./defaults.js";
export { Lexer } from "./Lexer.js";
export { Parser } from "./Parser.js";
export { Tokenizer } from "./Tokenizer.js";
export { Renderer } from "./Renderer.js";
export { TextRenderer } from "./TextRenderer.js";
export { Slugger } from "./Slugger.js";
export { Hooks } from "./Hooks.js";
